/*
 * Copyright (C) 2007-2010 Geometer Plus <contact@geometerplus.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

package org.geometerplus.fbreader.formats.plucker;

import org.geometerplus.fbreader.bookmodel.BookModel;
import org.geometerplus.fbreader.bookmodel.BookReader;
import org.geometerplus.fbreader.bookmodel.FBTextKind;
import org.geometerplus.fbreader.formats.pdb.DocDecompressor;
import org.geometerplus.fbreader.formats.pdb.PdbHeader;
import org.geometerplus.fbreader.formats.pdb.PdbUtil;
import org.geometerplus.zlibrary.core.filesystem.ZLFile;
import org.geometerplus.zlibrary.core.image.ZLImage;
import org.geometerplus.zlibrary.core.util.ZLInputStreamWithOffset;
import org.geometerplus.zlibrary.text.model.ZLTextParagraph;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

public class PluckerBookReader extends BookReader {
    private final ZLFile myFile;
    private final int myFileSize;
    private ZLInputStreamWithOffset myStream;
    private int myFont;
    private char[] myCharBuffer;
    private String myConvertedTextBuffer;
    private boolean myParagraphStarted = false;
    private boolean myBufferIsEmpty;
    //private	ZLTextForcedControlEntry myForcedEntry;
    private final ArrayList/*<std::pair<FBTextKind,bool> >*/ myDelayedControls = new ArrayList();
    private final ArrayList/*<std::string> */myDelayedHyperlinks = new ArrayList();
    private short myCompressionVersion;
    private char myBytesToSkip;

    private final ArrayList/*<std::pair<int, int> >*/ myReferencedParagraphs = new ArrayList();
    private final HashMap/*<int, std::vector<int> >*/ myParagraphMap = new HashMap(); /*<int, vector<pair<int, int>>>*/
    private ArrayList/*<Integer, Integer>*/ myParagraphVector = new ArrayList();
    private boolean myParagraphStored;

    public PluckerBookReader(ZLFile file, BookModel model, String encoding) {
        super(model);
        //myConverter = new EncodedTextReader(encoding).getConverter();
        myFile = file;
        myFileSize = (int) file.size();
        //System.out.println(filePath + "  " + encoding);
        myFont = FontType.FT_REGULAR;
        myCharBuffer = new char[65535];
        //myForcedEntry = null;
    }

    public boolean readDocument() {
        try {
            myStream = new ZLInputStreamWithOffset(myFile.getInputStream());

            PdbHeader header = new PdbHeader(myStream);

            setMainTextModel();
            myFont = FontType.FT_REGULAR;

            for (int index = 0; index < header.Offsets.length; ++index) {
                int currentOffset = myStream.offset();
                int pit = header.Offsets[index];
                if (currentOffset > pit) {
                    break;
                }
                //myStream.seek(pit - currentOffset, false);
                myStream.skip(pit - currentOffset);

                if (myStream.offset() != pit) {
                    break;
                }
                int recordSize = ((index != header.Offsets.length - 1) ? header.Offsets[index + 1] : myFileSize) - pit;
                readRecord(recordSize);
            }
            myStream.close();
        } catch (IOException e) {
            return false;
        }

        for (Iterator it = myReferencedParagraphs.iterator(); it.hasNext();) {
            Pair pair = (Pair) it.next();
            int first = (Integer) pair.myFirst;
            int second = (Integer) pair.mySecond;
            ArrayList/*<Integer>*/ list = (ArrayList) myParagraphMap.get(first);
            if (list != null) {
                for (int k = second; k < list.size(); ++k) {
                    if (((Integer) ((Pair) list.get(k)).myFirst) != -1) {
                        //addHyperlinkLabel(fromNumber(first) + '#' + fromNumber(second), (Integer)list.get(k));
                        final Pair p = (Pair) list.get(k);
                        //addHyperlinkLabel(fromNumber(first) + '#' + fromNumber(second), (Integer) p.mySecond, (Integer) p.myFirst);
                        break;
                    }
                }
            }
        }
        myReferencedParagraphs.clear();
        myParagraphMap.clear();
        return true;
    }

    private class FontType {
        public static final int FT_REGULAR = 0;
        public static final int FT_H1 = 1;
        public static final int FT_H2 = 2;
        public static final int FT_H3 = 3;
        public static final int FT_H4 = 4;
        public static final int FT_H5 = 5;
        public static final int FT_H6 = 6;
        public static final int FT_BOLD = 7;
        public static final int FT_TT = 8;
        public static final int FT_SMALL = 9;
        public static final int FT_SUB = 10;
        public static final int FT_SUP = 11;
    }

    ;

    private void readRecord(int recordSize) throws IOException {
        int uid = PdbUtil.readShort(myStream);
        if (uid == 1) {
            myCompressionVersion = (short) PdbUtil.readShort(myStream);
        } else {
            int paragraphs = PdbUtil.readShort(myStream);

            int size = PdbUtil.readShort(myStream);
            //TODO ??????  
            int type = myStream.read();

            int flags = myStream.read();

            switch (type) {
                case 0: // text (TODO: found sample file and test this code)
                case 1: // compressed text
                {
                    ArrayList/*<Integer>*/ pars = new ArrayList();
                    for (int i = 0; i < paragraphs; ++i) {
                        int pSize = PdbUtil.readShort(myStream);
                        pars.add(pSize);
                        myStream.skip(2);
                    }

                    boolean doProcess = false;
                    if (type == 0) {//?
                        byte[] buf = new byte[size];
                        doProcess = myStream.read(buf, 0, (int) size) == size;
                        if (doProcess) {
                            // TODO: use encoding!!!!
                            // TODO: don't create any new objects!!!!
                            myCharBuffer = new String(buf).toCharArray();
                        }
                    } else if (myCompressionVersion == 1) {
                        byte[] buf = new byte[size];
                        doProcess =
                                DocDecompressor.decompress(myStream, buf, recordSize - 8 - 4 * paragraphs) == size;
                        if (doProcess) {
                            myCharBuffer = new String(buf).toCharArray();
                        }
                    } else if (myCompressionVersion == 2) {
                        byte input[] = new byte[(int) (recordSize - 10 - 4 * paragraphs)];
                        final int inputSize = myStream.read(input);
                        Inflater decompressor = new Inflater();
                        decompressor.setInput(input, 0, inputSize);
                        byte output[] = new byte[size];
                        try {
                            doProcess = decompressor.inflate(output) == size;
                            decompressor.end();
                            myCharBuffer = new String(output, 0, size).toCharArray();
                        } catch (DataFormatException e) {
                            // TODO Auto-generated catch block
                            //	e.printStackTrace();
                            System.out.println(e.getMessage());
                        }
                        //doProcess =
                        //ZLZDecompressor(recordSize - 10 - 4 * paragraphs).
                        //decompress(myStream, myCharBuffer, size) == size;
                    }
                    if (doProcess) {
                        addHyperlinkLabel(fromNumber(uid));
                        myParagraphMap.put(uid, new ArrayList());
                        myParagraphVector = (ArrayList) myParagraphMap.get(uid);
                        processTextRecord(size, pars);
                        if ((flags & 0x1) == 0) {
//							insertEndOfTextParagraph();
                            //setNewTextModel();
                        }
                    }
                    break;
                }
                case 2: // image
                case 3: // compressed image
                {
                    final String mime = "image/palm";
                    ZLImage image = null;
                    if (type == 2) {
                        System.out.println("non-compressed image");
                        image = new PluckerFileImage(mime, myFile, myStream.offset(), recordSize - 8);
                    } else if (myCompressionVersion == 1) {
                        System.out.println("DocCompressedImage");
                        image = new DocCompressedFileImage(mime, myFile, myStream.offset(), recordSize - 8);
                    } else if (myCompressionVersion == 2) {
                        System.out.println("ZCompressedImage");
                        image = new ZCompressedFileImage(mime, myFile, myStream.offset() + 2, recordSize - 10);
                    }
                    if (image != null) {
                        addImage(fromNumber(uid), image);
                    }
                    break;
                }
                case 9: // category record is ignored
                    break;
                case 10:
                    short typeCode = (short) PdbUtil.readShort(myStream);
                    break;
                case 11: // style sheet record is ignored
                    break;
                case 12: // font page record is ignored
                    break;
                case 13: // TODO: process tables
                case 14: // TODO: process tables
                    break;
                case 15: // multiimage
                {
                    short columns = (short) PdbUtil.readShort(myStream);
                    short rows = (short) PdbUtil.readShort(myStream);
                    System.out.println("multiimage");
                    /*PluckerMultiImage image = new PluckerMultiImage(rows, columns, Model.getImageMap());
                         for (int i = 0; i < size / 2 - 2; ++i) {
                             short us = (short)myStream.read();
                             PdbUtil.readShort(myStream, us);
                             image.addId(fromNumber(us));
                         }
                         addImage(fromNumber(uid), image);
                         */
                    break;
                }
                default:
                    //std::cerr << "type = " << (int)type << "\n";
                    break;
            }
        }
    }

    private void processTextRecord(int size, ArrayList/*<Integer>*/ pars) {
        int start = 0;
        int end = 0;

        for (Iterator it = pars.iterator(); it.hasNext();) {
            start = end;
            end = start + (Integer) it.next();
            if (end > size) {
                return;
            }
            myParagraphStored = false;
            processTextParagraph(myCharBuffer, start, end);
            if (!myParagraphStored) {
                myParagraphVector.add(new Pair(-1, -1));
            }
        }
    }

    private void processTextParagraph(char[] data, int start, int end) {
        changeFont(FontType.FT_REGULAR);
        while (popKind()) {
        }

        myParagraphStarted = false;
        myBytesToSkip = 0;

        int textStart = start;
        boolean functionFlag = false;
        for (int ptr = start; ptr < end; ++ptr) {
            if (data[ptr] == 0) {
                functionFlag = true;
                if (ptr > textStart) {
                    safeBeginParagraph();
                    //			myConvertedTextBuffer = "";//.erase();
                    myConvertedTextBuffer = "";//myConverter.convert(data, textStart, ptr);
                    addData(myConvertedTextBuffer.toCharArray());
                    myBufferIsEmpty = false;
                }
            } else if (functionFlag) {
                int paramCounter = (data[ptr]) % 8;
                if (end - ptr > paramCounter) {
                    processTextFunction(data, ptr);
                    ptr += paramCounter;
                } else {
                    ptr = end - 1;
                }
                functionFlag = false;
                if (myBytesToSkip > 0) {
                    ptr += myBytesToSkip;
                    myBytesToSkip = 0;
                }
                textStart = ptr + 1;
            } else {
                if (data[ptr] == 0xA0) {
                    data[ptr] = 0x20;
                }
                if (!myParagraphStarted && (textStart == ptr) && (data[ptr] == ' ')) {
                    ++textStart;
                }
            }
        }
        if (end > textStart) {
            safeBeginParagraph();
            //	myConvertedTextBuffer = "";//erase();
            myConvertedTextBuffer = "";//myConverter.convert(data, textStart, end);
            addData(myConvertedTextBuffer.toCharArray());
            myBufferIsEmpty = false;
        }
        safeEndParagraph();
        //if (myForcedEntry != null) {
        //	myForcedEntry = null;
        //}
        myDelayedControls.clear();
    }

    private void processTextFunction(char[] ptr, int cur) {
        switch (ptr[cur]) {
            case 0x08:
                safeAddControl(FBTextKind.INTERNAL_HYPERLINK, false);
                break;
            case 0x0A:
                safeAddHyperlinkControl(fromNumber(twoBytes(ptr, cur + 1)));
                break;
            case 0x0C: {
                int sectionNum = twoBytes(ptr, cur + 1);
                int paragraphNum = twoBytes(ptr, cur + 3);
                safeAddHyperlinkControl(fromNumber(sectionNum) + '#' + fromNumber(paragraphNum));
                myReferencedParagraphs.add(new Pair(sectionNum, paragraphNum));
                break;
            }
            case 0x11:
                changeFont((ptr[cur + 1]));
                break;
            case 0x1A:
                safeBeginParagraph();
                //		System.out.println("image ref");
                addImageReference(fromNumber(twoBytes(ptr, cur + 1)), (short) 0);
                break;
            case 0x22:
                if (!myParagraphStarted) {
                    //if (myForcedEntry == null) {
                    //	myForcedEntry = new ZLTextForcedControlEntry();
                    //}
                    //myForcedEntry.setLeftIndent((short)ptr[cur + 1]);
                    //myForcedEntry.setRightIndent((short)ptr[cur + 2]);
                }
                break;
            case 0x29:
                if (!myParagraphStarted) {
                    //if (myForcedEntry == null) {
                    //	myForcedEntry = new ZLTextForcedControlEntry();
                    //}
                    //switch (ptr[cur + 1]) {
                    //	case 0: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_LEFT); break;
                    //	case 1: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_RIGHT); break;
                    //	case 2: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_CENTER); break;
                    //	case 3: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_JUSTIFY); break;
                    //}
                }
                break;
            case 0x33: // just break line instead of horizontal rule (TODO: draw horizontal rule?)
                safeEndParagraph();
                break;
            case 0x38:
                safeEndParagraph();
                break;
            case 0x40:
                safeAddControl(FBTextKind.EMPHASIS, true);
                break;
            case 0x48:
                safeAddControl(FBTextKind.EMPHASIS, false);
                break;
            case 0x53: // color setting is ignored
                break;
            case 0x5C:
                //			System.out.println("image ref");
                addImageReference(fromNumber(twoBytes(ptr, cur + 3)), (short) 0);
                break;
            case 0x60: // underlined text is ignored
                break;
            case 0x68: // underlined text is ignored
                break;
            case 0x70: // strike-through text is ignored
                break;
            case 0x78: // strike-through text is ignored
                break;
            case 0x83: {
                safeBeginParagraph();
                addData(new char[]{(char) twoBytes(ptr, cur + 2)});
                myBufferIsEmpty = false;
                myBytesToSkip = ptr[cur + 1];
                break;
            }
            case 0x85: // TODO: process 4-byte unicode character
                break;
            case 0x8E: // custom font operations are ignored
            case 0x8C:
            case 0x8A:
            case 0x88:
                break;
            case 0x90: // TODO: add table processing
            case 0x92: // TODO: process table
            case 0x97: // TODO: process table
                break;
            default: // this should be impossible
                //std::cerr << "Oops... function #" << (int)(unsigned char)*ptr << "\n";
                break;
        }
    }

    private void setFont(int font, boolean start) {
        switch (font) {
            case FontType.FT_REGULAR:
                break;
            case FontType.FT_H1:
            case FontType.FT_H2:
            case FontType.FT_H3:
            case FontType.FT_H4:
            case FontType.FT_H5:
            case FontType.FT_H6:
                processHeader(font, start);
                break;
            case FontType.FT_BOLD:
                safeAddControl(FBTextKind.BOLD, start);
                break;
            case FontType.FT_TT:
                safeAddControl(FBTextKind.CODE, start);
                break;
            case FontType.FT_SMALL:
                break;
            case FontType.FT_SUB:
                safeAddControl(FBTextKind.SUB, start);
                break;
            case FontType.FT_SUP:
                safeAddControl(FBTextKind.SUP, start);
                break;
        }
    }

    private void changeFont(int font) {
        if (myFont == font) {
            return;
        }
        setFont(myFont, false);
        myFont = font;
        setFont(myFont, true);
    }

    private void safeAddControl(byte kind, boolean start) {
        if (myParagraphStarted) {
            addControl((Byte) kind, (Boolean) start);
        } else {
            myDelayedControls.add(new Pair(kind, start));
        }
    }

    private void safeAddHyperlinkControl(String id) {
        if (myParagraphStarted) {
            addHyperlinkControl(FBTextKind.INTERNAL_HYPERLINK, id);
        } else {
            myDelayedHyperlinks.add(id);
        }
    }

    private void safeBeginParagraph() {
        if (!myParagraphStarted) {
            myParagraphStarted = true;
            myBufferIsEmpty = true;
            beginParagraph(ZLTextParagraph.Kind.TEXT_PARAGRAPH);
            if (!myParagraphStored) {
                //final ArrayList models = Model.getBookTextModels();
                //myParagraphVector.add(new Pair(((ZLTextPlainModel) models.get(models.size()-1)/*BookTextModel*/).getParagraphsNumber() - 1, models.size() - 1));
                myParagraphStored = true;
            }
            for (Iterator it = myDelayedControls.iterator(); it.hasNext();) {
                Pair pit = (Pair) it.next();
                addControl((Byte) pit.myFirst, (Boolean) pit.mySecond);
            }
            //if (myForcedEntry != null) {
            //	addControl(myForcedEntry);
            //} else {
            addControl(FBTextKind.REGULAR, true);
            //}
            for (Iterator it = myDelayedHyperlinks.iterator(); it.hasNext();) {
                addHyperlinkControl(FBTextKind.INTERNAL_HYPERLINK, (String) it.next());
            }
            myDelayedHyperlinks.clear();
        }
    }

    private void safeEndParagraph() {
        if (myParagraphStarted) {
            if (myBufferIsEmpty) {
                final String SPACE = " ";
                addData(SPACE.toCharArray());
            }
            endParagraph();
            myParagraphStarted = false;
        }
    }

    private void processHeader(int font, boolean start) {
        if (start) {
            enterTitle();
            int kind;
            switch (font) {
                case FontType.FT_H1:
                    kind = FBTextKind.H1;
                    break;
                case FontType.FT_H2:
                    kind = FBTextKind.H2;
                    break;
                case FontType.FT_H3:
                    kind = FBTextKind.H3;
                    break;
                case FontType.FT_H4:
                    kind = FBTextKind.H4;
                    break;
                case FontType.FT_H5:
                    kind = FBTextKind.H5;
                    break;
                case FontType.FT_H6:
                default:
                    kind = FBTextKind.H6;
                    break;
            }
            pushKind((byte) kind);
        } else {
            popKind();
            exitTitle();
        }
    }

    static private class Pair {
        public Object myFirst;
        public Object mySecond;

        Pair(Object first, Object second) {
            this.myFirst = first;
            this.mySecond = second;
        }
    }

    static private int twoBytes(char[] ptr, int offset) {
        return 256 * ptr[offset] + ptr[offset + 1];
    }

    static String fromNumber(int num) {
        String str = "";
        str += num;
        //ZLStringUtil.appendNumber(str, num);
        return str;
    }
}
